package net.pulseproject.commons.midi.collection;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import net.pulseproject.commons.midi.Receivable;
import net.pulseproject.commons.midi.entity.MidiSocket;
import net.pulseproject.router.modules.useable.MidiTransmitterModule;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class ReceiverCollection {

	private static final Log LOG = LogFactory.getLog(ReceiverCollection.class);
	
	private final
		Map<String/*port*/,
			Map<Integer/*channel*/,
				Map<Integer/*controllerNumber*/,
					Collection<Receivable>>>> 
		receiversByPort = new HashMap<String, Map<Integer, Map<Integer, Collection<Receivable>>>>();
	
	private final Map<MidiSocket, Collection<Receivable>> cachedResults =
		new HashMap<MidiSocket, Collection<Receivable>>();
	
	
	public ReceiverCollection(final Collection<MidiTransmitterModule> transmitters) {
		LOG.info("new ReceiverCollection(transmitters.size=" + transmitters.size() + ")");
		
		for (final MidiTransmitterModule currentTransmitter : transmitters) {
			this.initTransmitterModule(currentTransmitter);
		}
	}
	
	private void initTransmitterModule(final MidiTransmitterModule currentTransmitter) {
		LOG.debug("initTransmitterModule(" + currentTransmitter + ")");
		
		final MidiSocket socket = currentTransmitter.getSocket();
		final Collection<Receivable> receivers = this.initForReceiversAndAll(socket);
		final Collection<Receivable> outgoingModules = currentTransmitter.getOutgoingModules();
		
		LOG.trace("  ... adding for " + socket + " receivers: "  + Arrays.toString(outgoingModules.toArray()));
		receivers.addAll(outgoingModules);
	}
	
	private Collection<Receivable> initForReceiversAndAll(final MidiSocket socket) {
		final Map<Integer/*channel*/, Map<Integer/*controller*/, Collection<Receivable>>> receiversByChannel =
			this.initForPort(socket);
		final Map<Integer/*controller*/, Collection<Receivable>> receiversByController =
			this.initForChannel(socket, receiversByChannel);
		return this.initForController(socket, receiversByController);
	}
	
	private Map<Integer/*channel*/, Map<Integer/*controller*/, Collection<Receivable>>> initForPort(
			final MidiSocket socket) {
		final String port = socket.getPort();
		if(this.receiversByPort.containsKey(port) == false) {
			this.receiversByPort.put(port, new HashMap<Integer/*channel*/, Map<Integer/*controller*/, 
					Collection<Receivable>>>());
		}
		return this.receiversByPort.get(port);
	}
	
	private Map<Integer/*controller*/, Collection<Receivable>> initForChannel(final MidiSocket socket,
			final Map<Integer/*channel*/, Map<Integer/*controller*/, Collection<Receivable>>> receiversByChannel) {
		final Integer channel = socket.getChannel();
		if(receiversByChannel.containsKey(channel) == false) {
			receiversByChannel.put(channel, new HashMap<Integer/*controller*/, Collection<Receivable>>());
		}
		return receiversByChannel.get(channel);
	}
	
	private Collection<Receivable> initForController(final MidiSocket socket,
			final Map<Integer/*controller*/, Collection<Receivable>> receiversByController) {
		final Integer controllerNumber = socket.getController();
		if(receiversByController.containsKey(controllerNumber) == false) {
			receiversByController.put(controllerNumber, new HashSet<Receivable>());
		}
		return receiversByController.get(socket.getController());
	}
	
	
	public final Collection<Receivable> findReceivables(final MidiSocket socket) {
		LOG.debug("findReceivables(socket=" + socket + ")");
		
		final Collection<Receivable> storedResult = this.cachedResults.get(socket);
		if(storedResult != null) {
			return storedResult;
		}
		
		final Collection<Receivable> result = this.reallyFindReceivable(socket);
		this.cachedResults.put(socket, result);
		return result;
	}
	
	private Collection<Receivable> reallyFindReceivable(final MidiSocket socket) {
		final String port = socket.getPort();
		final Map<Integer/*channel*/, Map<Integer/*controller*/, Collection<Receivable>>> receiversByChannel =
			this.receiversByPort.get(port);
		
		if(receiversByChannel == null) {
			throw this.createException(socket, this.receiversByPort.keySet(), "port");
		}
		
		final Integer channel = socket.getChannel();
		final Map<Integer/*controller*/, Collection<Receivable>> receiversByController = 
			receiversByChannel.get(channel);
		
		if(receiversByController == null) {
			throw this.createException(socket, receiversByChannel.keySet(), "channel");
		}

		final Integer controller = socket.getController();
		final Collection<Receivable> allProperReceivers = receiversByController.get(controller);
		
		if(allProperReceivers == null) {
			return new HashSet<Receivable>(0);
		}
		
		return Collections.unmodifiableCollection(allProperReceivers);
	}
	
	private RuntimeException createException(final MidiSocket socket, final Set<?> set, final String thingyType) {
		return new RuntimeException("Could not find receivers by " + thingyType + "! Socket was: " + socket + "; " +
				"There were receivers for following " + thingyType + "s: " + Arrays.toString(set.toArray()));
	}
}
